package com.ccjk.beusoft.thirdlib.plugin1;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.widget.Toast;

import com.ccjk.beusoft.thirdlib.BuildConfig;

import java.io.File;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;

import dalvik.system.DexClassLoader;

/**
 * Created by CCJK on 2017/12/21.
 * 这是激进的方案，多classloader,每个插件都有自己的classloader
 */
public class PluginManager1 {
    private Context hostContext;
    private File dexOutputDir;
    private HashMap<String, Object> pluginLoadedApks = new HashMap<>();
    private HashMap<String, PackageInfo> pluginPackages = new HashMap<>();

    public static void toast(String s) {
        Toast.makeText(get().hostContext, s, Toast.LENGTH_SHORT).show();
    }

    public PackageInfo getPluginPackageInfo(String packageName) {
        return pluginPackages.get(packageName);
    }

    public Object getLoadedApk(String packageName) {
        return pluginLoadedApks.get(packageName);
    }


    public void loadApk(String apkPath) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException, InstantiationException {
        PackageInfo packageInfo = hostContext.getPackageManager().getPackageArchiveInfo(apkPath,
                PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
        if (packageInfo == null) {
            throw new RuntimeException("没有找到插件，或者插件包有问题: " + apkPath);
        }
        if (pluginLoadedApks.get(packageInfo.packageName) != null) {
            toast("插件已经加载");
            return;
        }
        //构造插件的LoadedApk
        Object pluginLoadedApk = createPluginLoadedApk(apkPath);
        //将loadedApk中classloader替换成插件的classloader
        replaceLoadedApkClassLoader(pluginLoadedApk, apkPath);
        //保存一份
        pluginLoadedApks.put(packageInfo.packageName, pluginLoadedApk);
        addLoadedApkToMap(packageInfo.packageName);
    }

    public void addLoadedApkToMap(String packageName) {
        try {
            //将插件的LoadedApk加入到ActivityThread中mPackages 中，使得在启动插件activity的时候系统能帮我们自动加载
            Object loadedApk = pluginLoadedApks.get(packageName);
            if (loadedApk != null) {
                Object activityThread = getActivityThread();
                ArrayMap mPackages = getActivityThreadMPackages(activityThread);
                //
                mPackages.put(packageName, new WeakReference(loadedApk));

                Field mResourcePackages = activityThread.getClass().getDeclaredField("mResourcePackages");
                mResourcePackages.setAccessible(true);
                ArrayMap map = (ArrayMap) mResourcePackages.get(activityThread);
                map.put(packageName, new WeakReference(loadedApk));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }


    private void replaceLoadedApkClassLoader(Object loadedApk, String apkPath) throws NoSuchFieldException, IllegalAccessException {
        ClassLoader pluginClassLoader = new DexClassLoader(apkPath, dexOutputDir.getAbsolutePath(),
                null, ClassLoader.getSystemClassLoader());

        try {
            Field mClassLoaderField = loadedApk.getClass().getDeclaredField("mClassLoader");
            mClassLoaderField.setAccessible(true);
            mClassLoaderField.set(loadedApk, pluginClassLoader);
            Log.e("replace", "ok");
        } catch (Exception e) {
            e.printStackTrace();
        }

    }


    private Object createPluginLoadedApk(String apkPath) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        //使用ActivityThread getPackageInfoNoCheck 来获取loadedApk
        /**
         *  public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
         CompatibilityInfo compatInfo)
         */

        Object compatInfo = getCompatInfo();
        ApplicationInfo applicationInfo = getApplicationInfo(apkPath);
        //
        Class activityThreadClass = Class.forName("android.app.ActivityThread");
        Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod("getPackageInfoNoCheck"
                , new Class[]{ApplicationInfo.class, Class.forName("android.content.res.CompatibilityInfo")});
        getPackageInfoNoCheckMethod.setAccessible(true);

        Object loadedApk = getPackageInfoNoCheckMethod.invoke(getActivityThread(), applicationInfo, compatInfo);
        return loadedApk;
    }

    /**
     * 这里使用 PackageParser
     * public static ApplicationInfo generateApplicationInfo(Package p, int flags,
     * PackageUserState state)
     * 方法来获取，注意： 这里需要做兼容，PackageParser 有很多版本，
     *
     * @param apkPath
     * @return ApplicationInfo
     */
    private ApplicationInfo getApplicationInfo(String apkPath) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
        Class packageParserClass = Class.forName("android.content.pm.PackageParser");
        //1.create Package 通过PackageParser parsePackage(File packageFile, int flags) 方法获取
        Class packageClass = Class.forName("android.content.pm.PackageParser$Package");
        Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage",
                new Class[]{File.class, int.class});
        parsePackageMethod.setAccessible(true);
        Object packageParser = packageParserClass.newInstance();
        Object p = parsePackageMethod.invoke(packageParser, new File(apkPath), 0);

        //2.selected flags


        //3.PackageUserState
        Class packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
        Object state = packageUserStateClass.newInstance();


        //
        Method generateApplicationInfoMethod = packageParserClass.getDeclaredMethod("generateApplicationInfo",
                new Class[]{packageClass, int.class, packageUserStateClass});
        generateApplicationInfoMethod.setAccessible(true);
        ApplicationInfo info = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser, p, 0, state);
        info.publicSourceDir = apkPath;
        info.sourceDir = apkPath;
        return info;
    }


    public Object getCompatInfo() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        Class clazz = Class.forName("android.content.res.CompatibilityInfo");
        Field DEFAULT_COMPATIBILITY_INFO_Field = clazz.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
        DEFAULT_COMPATIBILITY_INFO_Field.setAccessible(true);
        return DEFAULT_COMPATIBILITY_INFO_Field.get(null);
    }

    private ArrayMap getActivityThreadMPackages(Object activityThread) throws NoSuchFieldException, IllegalAccessException {
        Field mPackagesField = activityThread.getClass().getDeclaredField("mPackages");
        mPackagesField.setAccessible(true);
        return (ArrayMap) mPackagesField.get(activityThread);
    }


    private Object getActivityThread() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        Class activityThreadClass = Class.forName("android.app.ActivityThread");
        Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThreadMethod.setAccessible(true);
        Object activityThread = currentActivityThreadMethod.invoke(null);
        return activityThread;
    }

    //api

    /**
     * 是否是宿主程序的activity
     */
    public boolean isHost(String packageName) {
        return TextUtils.equals(BuildConfig.APPLICATION_ID, packageName);
    }


    /////////////////////////////

    private PluginManager1() {

    }

    public void init(Context context) {
        hostContext = context.getApplicationContext();
        dexOutputDir = new File(context.getCacheDir().getParent(), "plugin");
        if (!dexOutputDir.exists()) {
            dexOutputDir.mkdirs();
        }
    }


    private static class Instance {
        private static PluginManager1 instance = new PluginManager1();
    }

    public static PluginManager1 get() {
        return Instance.instance;
    }


}
